package utils

import (
	"archive/zip"
	"bufio"
	"context"
	"errors"
	"fmt"
	"io"
	"log"
	"mime"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"
	"unicode"

	"github.com/google/uuid"
	"github.com/minio/minio-go/v7"
	"github.com/rs/zerolog"
	"github.com/spf13/cast"
	"golang.org/x/sync/errgroup"
)

func CheckBuckets(client *minio.Client) ([]minio.BucketInfo, error) {
	logger := zerolog.DefaultContextLogger
	buckets, err := client.ListBuckets(context.Background())
	if err != nil {
		logger.Error().Err(err).Msgf("无法列出存储桶: %+v", buckets)
		return nil, err
	}
	logger.Debug().Msgf("Minio存储桶:%+v", buckets)
	for _, bucket := range buckets {
		log.Printf("Minio存储桶名称- %s\n", bucket.Name)
	}

	return buckets, nil
}

func QueryBucketList(ctx context.Context, client *minio.Client) ([]string, error) {
	logger := zerolog.DefaultContextLogger
	var bucketNameList []string
	buckets, err := client.ListBuckets(ctx)
	if err != nil {
		logger.Error().Err(err).Msgf("无法列出存储桶: %+v", buckets)
		return nil, err
	}
	logger.Debug().Msgf("Minio存储桶:%+v", buckets)
	for _, bucket := range buckets {
		logger.Debug().Msgf("Minio存储桶名称===%s\n", bucket.Name)
		bucketNameList = append(bucketNameList, bucket.Name)
	}

	return bucketNameList, nil
}

func DetectMimeType(filePath string) (string, error) {
	logger := zerolog.DefaultContextLogger
	file, err := os.Open(filePath)
	if err != nil {
		logger.Error().Err(err).Msgf("打开文件===%s失败", filePath)
		return "", err
	}
	defer file.Close()

	// 复用全局 buffer 减少内存分配
	var bufferPool = sync.Pool{
		New: func() any {
			return make([]byte, 512)
		},
	}

	bufPtr := bufferPool.Get().(*[]byte)
	buffer := *bufPtr
	defer bufferPool.Put(bufPtr)

	n, err := file.Read(buffer)
	if err != nil && err != io.EOF {
		logger.Error().Err(err).Msg("读取文件失败失败")
		return "", fmt.Errorf("读取文件失败: %w", err)
	}

	// 确保传入 DetectContentType 的切片长度正确
	return http.DetectContentType(buffer[:n]), nil
}

// BucketIsExists 验证桶是否存在
//
// 参数
//
//   - ctx (context.Context): 上下文
//   - client (*minio.Client): minio客户端对象
//   - bucketName (string): 桶名称
//
// 返回
//
//   - error: 错误信息
func BucketIsExists(ctx context.Context, client *minio.Client, bucketName string) error {
	logger := zerolog.DefaultContextLogger
	exists, err := client.BucketExists(ctx, bucketName)
	if err != nil {
		logger.Error().Err(err).Msgf("检查存储桶===%s失败", bucketName)
		return fmt.Errorf("检查存储桶失败: %w", err)
	}

	if !exists {
		if err = client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{
			Region:        "us-east-1", // 明确指定区域
			ObjectLocking: false,       // 按需启用
		}); err != nil {
			logger.Error().Err(err).Msgf("创建存储桶失败===%s失败", bucketName)
			return fmt.Errorf("创建存储桶失败: %w", err)
		}
	}
	return nil
}

func GenerateObjectName(filePath string) string {
	ext := filepath.Ext(filePath)
	fileName := fmt.Sprintf("%s-%s%s",
		filepath.Base(filePath[:len(filePath)-len(ext)]),
		time.Now().Format("20060102150405"),
		ext,
	)
	zerolog.DefaultContextLogger.Info().Msgf("生成文件名: %s", fileName)
	return fileName
}

// ResolveMimeType 检测文件mime类型
func ResolveMimeType(filePath string) (string, error) {
	// 优先通过扩展名检测
	if mimeType := mime.TypeByExtension(filepath.Ext(filePath)); mimeType != "" {
		return mimeType, nil
	}

	// 兜底：内容嗅探
	mimeType, err := DetectMimeType(filePath)
	if err != nil {
		return "application/octet-stream", nil
	}
	return mimeType, nil
}

// GeneratePresignedURL 设置文件访问策略
//
// 参数
//
//   - ctx (context.Context): 上下文
//   - client (*minio.Client): minio客户端对象
//   - bucketName (string): 桶名称
//   - objectName (string): 文件名
//   - mimeType (string): 文件类型
//   - expires (time.Duration): 时限
//
// 返回
//
//   - string: 文件访问URL
//   - error: 错误信息
func GeneratePresignedURL(ctx context.Context, client *minio.Client, bucketName, objectName, mimeType string, expires time.Duration) (string, error) {
	// 安全设置：限制权限和有效期
	disposition := fmt.Sprintf("inline; filename=\"%s\"", url.QueryEscape(objectName))
	if expires == 0 {
		expires = time.Hour
	}
	u, err := client.PresignedGetObject(
		ctx,
		bucketName,
		objectName,
		expires,
		url.Values{
			"response-content-type":        []string{mimeType},
			"response-content-disposition": []string{disposition},
		},
	)
	if err != nil {
		zerolog.DefaultContextLogger.Error().AnErr("生成预签名URL失败", err).Msg("")
		return "", fmt.Errorf("生成预签名URL失败: %w", err)
	}

	return u.String(), nil
}

// UploadObj 上传
//
// 参数
//
//   - ctx (context.Context): 上下文
//   - client (*minio.Client): minio客户端对象
//   - bucketName (string): 桶名称
//   - objectName (string): 上传后显示的文件名
//   - filePath (string): 上传文件路径
//
// 返回
//
//   - string: 文件访问URL
//   - error: 错误信息
func UploadObj(ctx context.Context, client *minio.Client, bucketName, prefix, objectName, filePath string) (string, error) {
	logger := zerolog.DefaultContextLogger
	// 2. 自动生成对象名（如果未提供）
	if objectName == "" {
		objectName = GenerateObjectName(filePath) // 改用更可读的生成逻辑
	}

	// 如果有自定义前缀
	if prefix != "" {
		// 去掉前后的 "/"
		prefix = strings.Trim(prefix, "/")
		objectName = fmt.Sprintf("%s/%s", prefix, objectName)
	}

	// 桶不存在则创建
	if err := BucketIsExists(ctx, client, bucketName); err != nil {
		logger.Error().Err(err).Msgf("存储桶===%s操作失败", bucketName)
		return "", fmt.Errorf("存储桶操作失败: %w", err)
	}

	// 获取文件的 MIME 类型
	fileExt := filepath.Ext(filePath)
	mimeType, err := ResolveMimeType(filePath)
	if err != nil {
		logger.Error().Err(err).Msgf("检测文件类型===%s失败", filePath)
		return "", fmt.Errorf("检测文件类型失败: %w", err)
	}

	logger.Info().Msgf("上传文件MIME类型为: %s,后缀名: %s", mimeType, fileExt)

	uploadInfo, err := client.FPutObject(ctx, bucketName, objectName,
		filePath, minio.PutObjectOptions{
			ContentType: mimeType,
			NumThreads:  4, // 多线程分块上传（大文件优化）
		})
	if err != nil {
		logger.Error().Err(err).Msg("文件上传失败")
		return "", errors.New("文件上传失败")
	}

	logger.Info().Msgf("文件上传成功! ===%v", ToJsonFormat(uploadInfo))

	// inline:预览， attachment:下载
	previewUrl, err := GeneratePresignedURL(ctx, client, bucketName, objectName, mimeType, 0)
	if err != nil {
		logger.Error().Err(err).Msg("生成URL失败")
		return "", fmt.Errorf("生成URL失败: %w", err)
	}

	logger.Info().Msgf("返回临时分享URL===%s", previewUrl)
	return previewUrl, nil
}

// DownloadObj 下载
//
// 参数
//
//   - client (*minio.Client): minio客户端对象
//   - bucketName (string): 桶名称
//   - objectName (string): 需要下载的文件名
//
// 返回
//
//   - string: 文件下载成功返回的绝对路径
//   - error: 下载失败返回错误
func DownloadObj(client *minio.Client, bucketName, objectName string) (string, error) {
	logger := zerolog.DefaultContextLogger
	// 获取当前根目录
	workingDir, err := os.Getwd()
	if err != nil {
		logger.Error().Err(err).Msgf("获取工作目录失败: %v", workingDir)
		return "", err
	}

	// 拼接 download 文件夹路径和文件名
	downloadDir := filepath.Join(workingDir, "download")
	// 创建 download 文件夹（如果不存在的话）
	if _, err = os.Stat(downloadDir); os.IsNotExist(err) {
		err = os.Mkdir(downloadDir, os.ModePerm)
		if err != nil {
			logger.Error().Err(err).Msgf("创建 download 文件夹失败: %v", downloadDir)
			return "", err
		}
	}

	fileSuffix := filepath.Ext(objectName)
	logger.Debug().Msgf("下载文件fileSuffix: %s", fileSuffix)
	// 拼接最终的文件路径
	var filePath = filepath.Join(downloadDir, uuid.NewString()+fileSuffix)

	err = client.FGetObject(context.Background(), bucketName, objectName, filePath, minio.GetObjectOptions{})
	if err != nil {
		logger.Error().Err(err).Msgf("文件下载失败: %s", objectName)
		return "", err
	}
	logger.Debug().Msgf("文件下载成功!,路径为====%s", filePath)
	return filePath, nil
}

// UploadObjectSmall 上传小文件
//
// 参数
//
//   - ctx context.Context: 上下文
//   - client (*minio.Client): minio客户端对象
//   - bucketName (string): 桶名称
//   - prefix (string): 后缀
//   - objectName (string): 重命名文件名
//   - file (*multipart.FileHeader): 文件对象
//
// 返回
//
//   - string: 文件访问URL
//   - error: 返回错误
func UploadObjectSmall(ctx context.Context, client *minio.Client, bucketName, prefix, objectName string, file *multipart.FileHeader) (string, error) {
	logger := zerolog.DefaultContextLogger
	src, err := file.Open()
	if err != nil {
		logger.Error().Err(err).Msgf("minio打开上传文件===%s失败", file.Filename)
		return "", err
	}
	defer src.Close()

	fileName := file.Filename

	// 2. 自动生成对象名（如果未提供）
	if objectName == "" {
		objectName = GenerateObjectName(fileName)
	}

	// 如果有自定义前缀
	if prefix != "" {
		// 去掉前后的 "/"
		prefix = strings.Trim(prefix, "/")
		objectName = fmt.Sprintf("%s/%s", prefix, objectName)
	}

	// 桶不存在则创建
	if err = BucketIsExists(ctx, client, bucketName); err != nil {
		logger.Error().Err(err).Msgf("minio存储桶===%s操作错误", bucketName)
		return "", fmt.Errorf("存储桶操作失败: %w", err)
	}

	// 获取文件的 MIME 类型
	fileExt := filepath.Ext(fileName)
	mimeType, err := ResolveMimeType(fileName)
	if err != nil {
		logger.Error().Err(err).Msgf("minio文件fileExt==%s类型检测错误", fileExt)
		return "", fmt.Errorf("文件类型检测失败: %w", err)
	}

	logger.Info().Msgf("minio上传文件的MIME===%s后缀===%s", mimeType, fileExt)

	uploadInfo, err := client.PutObject(ctx, bucketName, objectName, src, file.Size, minio.PutObjectOptions{
		ContentType: mimeType,
		NumThreads:  4, // 多线程分块上传（大文件优化）
	})
	if err != nil {
		logger.Error().Err(err).Msgf("minio文件上传===%s失败", uploadInfo)
		return "", errors.New("文件上传失败")
	}
	logger.Info().Msgf("minio文件上传成功信息===%v", ToJsonFormat(uploadInfo))
	// inline:预览， attachment:下载
	// previewUrl, err := GeneratePresignedURL(ctx, client, bucketName, objectName, mimeType, 0)
	// if err != nil {
	// 	return "", fmt.Errorf("生成URL失败: %w", err)
	// }
	// zerolog.DefaultContextLogger.Info().Msgf("返回临时分享URL===%s", previewUrl)

	// 从minio客户端获取是否为https或者http访问地址及端口信息
	permanentURL := fmt.Sprintf("%s://%s/%s/%s", client.EndpointURL().Scheme, client.EndpointURL().Host, bucketName, objectName)
	logger.Info().Msgf("minio文件上传成功访问URL===%s", permanentURL)
	return permanentURL, nil
}

// UploadObjectSmallFromFile 上传文件
//
// 参数
//
//   - ctx (context.Context): 上下文
//   - client (*minio.Client): minio客户端对象
//   - bucketName (string): 桶名称
//   - prefix (string): 后缀
//   - objectName (string): 原始文件名
//   - fileName (string): 文件名
//   - file (multipart.File): 文件对象
//
// 返回
//
//   - string: 文件访问URL
//   - error: 错误信息
func UploadObjectSmallFromFile(ctx context.Context, client *minio.Client, bucketName, prefix, objectName, fileName string, file multipart.File) (string, error) {
	logger := zerolog.DefaultContextLogger
	if objectName == "" {
		objectName = GenerateObjectName(fileName)
	}

	if prefix != "" {
		prefix = strings.Trim(prefix, "/")
		objectName = fmt.Sprintf("%s/%s", prefix, objectName)
	}

	if err := BucketIsExists(ctx, client, bucketName); err != nil {
		logger.Error().Err(err).Msgf("存储桶===%s操作失败", bucketName)
		return "", fmt.Errorf("存储桶操作失败: %w", err)
	}

	mimeType, err := ResolveMimeType(fileName)
	if err != nil {
		logger.Error().Err(err).Msgf("文件===%s类型检测失败", fileName)
		return "", fmt.Errorf("文件类型检测失败: %w", err)
	}

	uploadInfo, err := client.PutObject(ctx, bucketName, objectName, file, -1, minio.PutObjectOptions{
		ContentType: mimeType,
		NumThreads:  4,
	})
	if err != nil {
		logger.Error().Err(err).Msgf("文件===%+v上传失败", uploadInfo)
		return "", fmt.Errorf("文件上传失败: %w", err)
	}

	logger.Info().Msgf("minio文件上传成功信息===%v", ToJsonFormat(uploadInfo))

	return fmt.Sprintf("%s://%s/%s/%s", client.EndpointURL().Scheme, client.EndpointURL().Host, bucketName, objectName), nil
}

// UploadObjectSmallFromReader 上传小文件
//
// 参数
//
//   - ctx context.Context: 上下文
//   - client (*minio.Client): minio客户端对象
//   - bucketName (string): 桶名称
//   - objectName (string): 原始文件名
//   - fileName (string): 重命名文件名
//   - reader (io.ReadSeeker): 文件读取器
//   - fileSize (string): 文件大小
//
// 返回
//
//   - string: 访问URL
//   - error: 返回错误
func UploadObjectSmallFromReader(ctx context.Context, client *minio.Client, bucketName, prefix, objectName, fileName string,
	reader io.ReadSeeker, fileSize int64) (string, error) {
	logger := zerolog.DefaultContextLogger
	if objectName == "" {
		objectName = GenerateObjectName(fileName)
	}

	if prefix != "" {
		prefix = strings.Trim(prefix, "/")
		objectName = fmt.Sprintf("%s/%s", prefix, objectName)
	}

	if err := BucketIsExists(ctx, client, bucketName); err != nil {
		logger.Error().Err(err).Msgf("存储桶===%s操作失败", bucketName)
		return "", fmt.Errorf("存储桶操作失败: %w", err)
	}

	mimeType, err := ResolveMimeType(fileName)
	if err != nil {
		logger.Error().Err(err).Msgf("文件===%s类型检测失败", fileName)
		return "", fmt.Errorf("文件类型检测失败: %w", err)
	}

	uploadInfo, err := client.PutObject(ctx, bucketName, objectName, reader, fileSize, minio.PutObjectOptions{
		ContentType: mimeType,
		NumThreads:  4,
	})
	if err != nil {
		logger.Error().Err(err).Msgf("文件===%+v上传失败", uploadInfo)
		return "", fmt.Errorf("文件上传失败: %w", err)
	}

	logger.Info().Msgf("minio文件上传成功信息===%v", ToJsonFormat(uploadInfo))

	return fmt.Sprintf("%s://%s/%s/%s", client.EndpointURL().Scheme, client.EndpointURL().Host, bucketName, objectName), nil
}

// RemoveObj 删除文件
//
// 参数
//
//   - ctx (context.Context): 上下文
//   - client (*minio.Client): minio客户端对象
//   - bucketName (string): 桶名称
//   - objectName (string): 需要删除的文件名
//
// 返回
//
//   - error: 删除失败返回错误
func RemoveObj(ctx context.Context, client *minio.Client, bucketName, objectName string) error {
	logger := zerolog.DefaultContextLogger
	if client == nil {
		logger.Error().Msg("MinIO客户端未初始化")
		return fmt.Errorf("MinIO客户端未初始化")
	}
	if bucketName == "" || objectName == "" {
		logger.Error().Msgf("MinIO客户端参数错误: bucket=%q, object=%q", bucketName, objectName)
		return fmt.Errorf("参数错误: bucket=%q, object=%q", bucketName, objectName)
	}

	exists, err := client.StatObject(ctx, bucketName, objectName, minio.StatObjectOptions{})
	logger.Info().Msgf("Minio检测文件是否存在===%v", ToJsonFormat(exists))

	if err != nil {
		logger.Error().Err(err).Msgf("MinIO检查对象状态失败: bucket=%q, object=%q", bucketName, objectName)
		// 判断是否是对象不存在错误
		var resp minio.ErrorResponse
		if errors.As(err, &resp) && resp.StatusCode == http.StatusNotFound {
			logger.Warn().Msgf("对象不存在: %s/%s，不执执行删除操作", bucketName, objectName)
			return nil
		}
		return fmt.Errorf("检查对象状态失败: %w", err)
	}

	if err = client.RemoveObject(ctx, bucketName, objectName, minio.RemoveObjectOptions{
		GovernanceBypass: true, // 如果启用了对象锁定，可能需要此选项
	}); err != nil {
		logger.Error().Err(err).Msgf("删除失败 [bucket=%s, object=%s]", bucketName, objectName)
		return fmt.Errorf("无法删除对象 %q: %w", objectName, err)
	}

	logger.Info().Msgf("Minio删除文件成功===%s/%s", bucketName, objectName)
	return nil
}

// RemoveObjList 批量删除文件
//
// 参数
//
//   - ctx context.Context: 上下文
//   - client (*minio.Client): minio客户端对象
//   - bucketName (string): 桶名称
//   - objectList ([]string): 需要删除的文件名切片
//
// 返回
//
//   - error: 返回错误
func RemoveObjList(ctx context.Context, client *minio.Client, bucketName string, objectList []string) error {
	logger := zerolog.DefaultContextLogger
	if len(objectList) <= 0 {
		logger.Error().Msg("文件名不能为空")
		return errors.New("文件名不能为空")
	}
	var (
		wg      sync.WaitGroup
		errChan = make(chan error, len(objectList))
	)

	concurrency := 5
	sem := make(chan struct{}, concurrency)

	// 设置超时为10秒钟
	ctxTimeout, cancel := context.WithTimeout(ctx, 10*time.Second)
	defer cancel()

	for _, obj := range objectList {
		wg.Add(1)
		sem <- struct{}{}
		go func(o string) {
			defer wg.Done()
			defer func() { <-sem }()
			if err := RemoveObj(ctxTimeout, client, bucketName, o); err != nil {
				errChan <- fmt.Errorf("文件 %q: %w", o, err)
			}
		}(obj)
	}

	wg.Wait()
	close(errChan)

	var errs []error
	for err := range errChan {
		errs = append(errs, err)
	}

	if len(errs) > 0 {
		return fmt.Errorf("部分删除失败: %v", errors.Join(errs...))
	}
	return nil
}

// UploadStream 通过流式上传文件到MinIO（适合大文件）
//
// 参数
//
//   - ctx: 上下文（支持取消）
//   - client: 初始化好的MinIO客户端
//   - bucket: 存储桶名称（需提前创建）
//   - object: 对象名称（包含路径）
//   - reader: 文件内容流
//   - size: 文件大小（字节，用于进度计算）
//
// 返回:
//   - 文件访问URL
//   - 错误信息
//
// goland:noinspection GoUnhandledErrorResult
func UploadStream(ctx context.Context, client *minio.Client, bucket, object string, reader io.Reader, size int64, forever bool) (string, error) {

	logger := zerolog.DefaultContextLogger
	// 1. 参数校验
	if client == nil {
		logger.Error().Msg("MinIO客户端未初始化")
		return "", fmt.Errorf("MinIO客户端未初始化")
	}
	if bucket == "" {
		logger.Error().Msg("MinIO存储桶名称不能为空")
		return "", fmt.Errorf("MinIO存储桶名称不能为空")
	}

	// 2. 自动生成对象名（如果未提供）
	if object == "" {
		object = GenerateObjectName("stream-upload")
	} else {
		object = GenerateObjectName(object)
	}

	startTime := time.Now()
	// 3. 创建带进度监控的Reader
	progressReader := &ProgressReader{
		Reader: reader,
		Size:   size,
		OnProgress: func(read, total int64) {
			logProgress(read, total, startTime)
		},
	}

	mimeType, _ := ResolveMimeType(object)

	// 4. 执行上传（带重试机制）
	uploadInfo, err := retryUpload(ctx, func() (minio.UploadInfo, error) {
		return client.PutObject(
			ctx,
			bucket,
			object,
			progressReader,
			size,
			minio.PutObjectOptions{
				ContentType: mimeType,                      // 根据扩展名推测
				NumThreads:  uint(GetOptimalThreads(size)), // 并发线程数
				PartSize:    128 << 20,                     // 128MB/分块
			},
		)
	})
	if err != nil {
		logger.Error().Msg("MinIO上传失败")
		return "", fmt.Errorf("上传失败: %w", err)
	}

	zerolog.DefaultContextLogger.Info().Msgf("Minio上传完成返回信息: %+v", ToJsonFormat(uploadInfo))

	// 可预览的文件类型（图片/PDF/文本等）
	previewType := map[string]bool{
		"image/jpeg":       true,
		"image/png":        true,
		"image/gif":        true,
		"image/webp":       true,
		"application/pdf":  true,
		"application/json": true,
		"text/plain":       true,
	}

	// inline:预览， attachment:下载
	disposition := "attachment"
	if previewType[mimeType] {
		disposition = "inline"
	}

	// 安全编码文件名，比QueryEscape更友好
	encodedName := url.PathEscape(object)

	// 如果设置永久有效
	if forever {
		return fmt.Sprintf("%s://%s/%s/%s", client.EndpointURL().Scheme, client.EndpointURL().Host, bucket, object), nil
	}

	// 5. 生成访问URL（7天有效期）
	presignedURL, err := client.PresignedGetObject(
		ctx,
		bucket,
		object,
		7*24*time.Hour,
		url.Values{
			"response-content-type":        []string{mimeType},
			"response-content-disposition": []string{fmt.Sprintf("%s; filename=\"%s\"", disposition, encodedName)},
		},
	)
	if err != nil {
		logger.Error().Msgf("MinIO生成访问链接失败===%s", object)
		return "", fmt.Errorf("生成访问链接失败: %w", err)
	}

	return presignedURL.String(), nil
}

// ProgressReader 包装原始Reader并添加进度监控
type ProgressReader struct {
	io.Reader
	Size         int64
	read         int64                         // 小写字母开头表示私有字段
	startTime    time.Time                     // 改为小写保持一致性
	hasCompleted bool                          // 标记是否已完成
	OnProgress   func(read int64, total int64) // 改为传参方式
}

func (r *ProgressReader) Read(p []byte) (n int, err error) {
	n, err = r.Reader.Read(p)
	r.read += int64(n)

	// 2. 未完成且(达到大小或超时)
	shouldReport := !r.hasCompleted &&
		(r.read == r.Size || time.Since(r.startTime) > time.Second)
	// 触发回调时传入当前状态
	if r.OnProgress != nil && shouldReport {
		r.OnProgress(r.read, r.Size)
		if r.read == r.Size {
			r.hasCompleted = true
		}
	}
	return
}

// SanitizeObjectName 清理非法字符并保留扩展名
func SanitizeObjectName(name string) string {
	ext := filepath.Ext(name)
	base := strings.TrimSuffix(name, ext)

	// 移除特殊字符
	base = strings.Map(func(r rune) rune {
		switch {
		case unicode.IsLetter(r) || unicode.IsNumber(r):
			return r
		case r == '-', r == '_', r == '.':
			return r
		default:
			return '-'
		}
	}, base)

	return base + ext
}

func logProgress(read, total int64, start time.Time) {
	if total <= 0 {
		total = 1 // 防止除零或负数
	}
	percent := float64(read) / float64(total) * 100
	if percent < 0 {
		percent = 0
	}
	if percent > 100 {
		percent = 100
	}
	speedMB := float64(read) / (1024 * 1024) / time.Since(start).Seconds()

	// 进度条可视化（终端友好）
	bar := progressBar(int(percent), 50)
	zerolog.DefaultContextLogger.Info().Msgf("[%s] %.2f%% %.2f MB/s", bar, percent, speedMB)
	if read == total {
		duration := time.Since(start)
		milliseconds := duration.Milliseconds()
		zerolog.DefaultContextLogger.Info().Msgf("Minio文件上传完成，平均速度: %.2f MB/s, 总耗时: %d ms",
			float64(total)/(1024*1024)/time.Since(start).Seconds(),
			milliseconds)
	}
}

func progressBar(percent, width int) string {
	if percent < 0 {
		percent = 0
	}
	if percent > 100 {
		percent = 100
	}

	filled := percent * width / 100
	if filled < 0 {
		filled = 0
	}
	if filled > width {
		filled = width
	}
	return strings.Repeat("=", filled) + strings.Repeat(" ", width-filled)
}

func GetOptimalThreads(size int64) int {
	count := 0
	// 根据文件大小动态调整
	switch {
	case size > 10<<30: // >10GB
		count = 12
	case size > 1<<30: // >1GB
		count = 8
	default:
		count = 4
	}
	zerolog.DefaultContextLogger.Info().Msgf("Minio并发线程数:%d", count)
	return count
}

// 重试上传（3次重试，指数退避）
func retryUpload(ctx context.Context, fn func() (minio.UploadInfo, error)) (minio.UploadInfo, error) {
	var lastErr error
	for i := 0; i < 3; i++ {
		select {
		case <-ctx.Done():
			return minio.UploadInfo{}, ctx.Err()
		default:
		}

		info, err := fn()
		if err == nil {
			return info, nil
		}
		lastErr = err
		time.Sleep(time.Second * time.Duration(1<<i)) // 指数退避
	}
	return minio.UploadInfo{}, fmt.Errorf("上传超过重试次数: %w", lastErr)
}

// HandlerUpload 处理上传
//
// 参数
//
//   - ctx (context.Context): 上下文
//   - minioClient (*minio.Client): minio客户端对象
//   - fileHeader (*multipart.FileHeader): 文件对象
//   - bucketName (string): 桶名称
//   - forever (bool): 是否永久保存
//
// 返回
//
//   - string: 访问URL
//   - error: 错误信息
//
//goland:noinspection GoUnhandledErrorResult
func HandlerUpload(ctx context.Context, minioClient *minio.Client, fileHeader *multipart.FileHeader, bucketName string, forever bool) (string, error) {
	logger := zerolog.DefaultContextLogger
	src, err := fileHeader.Open()
	if err != nil {
		logger.Error().Err(err).Msgf("打开文件失败: %+v", fileHeader.Filename)
		return "", errors.New("上传文件打开失败")
	}
	defer src.Close()

	// 调用流式上传
	respUrl, err := UploadStream(
		ctx,
		minioClient,
		bucketName,          // 存储桶
		fileHeader.Filename, // 对象路径
		src,                 // 文件流
		fileHeader.Size,     // 文件大小
		forever,
	)
	if err != nil {
		logger.Error().Err(err).Msgf("上传文件===%s到minio错误", fileHeader.Filename)
		return "", err
	}

	return respUrl, nil
}

// HandlerParallelUpload 处理并行上传
//
// 参数
//
//   - ctx (context.Context): 上下文
//   - minioClient (*minio.Client): minio客户端对象
//   - form (*multipart.Form): 表单
//   - bucketName (string): 桶名称
//
// 返回
//
//   - []string: 访问URL切片
//   - error: 错误信息
//
//goland:noinspection GoUnhandledErrorResult
func HandlerParallelUpload(ctx context.Context, minioClient *minio.Client,
	form *multipart.Form, bucketName string) ([]string, error) {
	// 获取所有文件
	fileList := form.File
	// 获取所有字符串参数
	fields := form.Value

	logger := zerolog.DefaultContextLogger
	logger.Debug().Msgf("多文件上传参数，文件参数===%v，字符串参数===%v", fileList, fields)

	if len(fileList) == 0 {
		zerolog.DefaultContextLogger.Error().Msgf("未获取到上传文件: %+v", fileList)
		return nil, errors.New("未获取到上传信息")
	}
	// 遍历所有字符串参数
	params := make(map[string]string)
	for key, values := range fields {
		if len(values) > 0 {
			params[key] = values[0]
		}
	}
	// 获取 forever 参数
	forever := false
	if v, ok := params["forever"]; ok {
		forever = cast.ToBool(v)
	}

	// 获取 prefix 参数
	prefix := ""
	if v, ok := params["prefix"]; ok {
		prefix = v
	}
	logger.Debug().Msgf("从表单获取的参数prefix===%s", prefix)

	// 准备并发上传
	type fileWithIndex struct {
		index int
		fh    *multipart.FileHeader
	}

	var (
		filesUploadIndexList []fileWithIndex
		index                = 0
	)

	for _, values := range fileList {
		for _, fh := range values {
			if fh != nil {
				filesUploadIndexList = append(filesUploadIndexList, fileWithIndex{index: index, fh: fh})
				index++
			}

			// 如果 prefix 不为空，只取第一个文件
			if prefix != "" && len(filesUploadIndexList) > 0 {
				break
			}
		}
		if prefix != "" && len(filesUploadIndexList) > 0 {
			break
		}
	}

	// 返回结果切片，保证顺序
	list := make([]string, len(filesUploadIndexList))

	var g errgroup.Group

	for _, f := range filesUploadIndexList {
		// 捕获循环变量
		file := f
		g.Go(func() error {
			logger.Debug().Msgf("上传文件索引=%d, filename=%s, size=%d", file.index, file.fh.Filename, file.fh.Size)
			var url string
			var err error
			if prefix != "" {
				url, err = UploadObjectSmall(
					ctx,
					minioClient,
					bucketName,
					prefix,
					"",
					file.fh,
				)
			} else {
				url, err = HandlerUpload(ctx, minioClient, file.fh, bucketName, forever)
			}
			if err != nil {
				return err
			}
			list[f.index] = url
			return nil
		})
	}

	if err := g.Wait(); err != nil {
		return nil, err
	}

	return list, nil
}

func GetStatObject(ctx context.Context, client *minio.Client, bucketName, objectName string) (minio.ObjectInfo, error) {
	return client.StatObject(ctx, bucketName, objectName, minio.StatObjectOptions{})
}

// GetObjByte 返回文件字节
//
// 参数
//
//   - ctx context.Context: 上下文
//   - client (*minio.Client): minio客户端对象
//   - writer (http.ResponseWriter): http响应写入
//   - bucketName (string): 桶名称
//   - objectName (string): 文件名
//   - dstPath (string): 文件名——流式使用
//   - thresholdBytes (int64): 缓冲大小
//
// 返回
//
//   - []byte: 字节
//   - written: 写入
//   - error: 返回错误
//
//goland:noinspection GoUnhandledErrorResult
func GetObjByte(ctx context.Context, writer http.ResponseWriter, client *minio.Client, bucketName,
	objectName, dstPath string, thresholdBytes int64) (data []byte, written int64, err error) {
	logger := zerolog.DefaultContextLogger
	if client == nil {
		logger.Error().Msg("MinIO客户端未初始化")
		return nil, 0, fmt.Errorf("MinIO客户端未初始化")
	}
	if objectName == "" {
		logger.Error().Msg("文件名不能为空")
		return nil, 0, errors.New("文件名不能为空")
	}

	// 先尝试获取对象元信息（大小）
	objInfo, err := client.StatObject(ctx, bucketName, objectName, minio.StatObjectOptions{})
	if err != nil {
		// 如果 StatObject 失败，记录日志并退回到“流式下载”策略
		logger.Warn().Err(err).Msg("StatObject 失败，尝试以流式方式下载")
		if dstPath != "" {
			written, err2 := streamToFile(ctx, client, bucketName, objectName, dstPath, 4*1024*1024)
			return nil, written, err2
		}
	}

	// 小文件 -> 读到内存
	if objInfo.Size <= thresholdBytes {
		obj, err := client.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
		if err != nil {
			return nil, 0, err
		}
		defer obj.Close()

		// b, err := io.ReadAll(obj)
		// if err != nil {
		// 	return nil, 0, err
		// }

		// 写到目标文件（可选）
		// if err := os.WriteFile(dstPath, b, 0644); err != nil {
		// 	return nil, 0, err
		// }
		// 直接复制到 writer
		written, err = io.Copy(writer, obj)
		if err != nil {
			return nil, written, err
		}
		return nil, written, nil
	} else {
		// 大文件 -> 流式写入
		if dstPath == "" {
			// 生成文件路径
			dstPath = url.PathEscape(fmt.Sprintf("%s_%s%s",
				strings.TrimSuffix(objectName, filepath.Ext(objectName)),
				NormalUUID(),
				filepath.Ext(objectName)))
			logger.Warn().Msgf("文件大小 %d 超过阈值 %d，但未提供目标路径", objInfo.Size, thresholdBytes)
		}
		logger.Debug().Msgf("文件大小==%d超过阈值===%d使用流式方式传输文件===%s到响应", objInfo.Size, thresholdBytes, dstPath)
		written, err := StreamObjectToWriter(ctx, writer, client, bucketName, objectName, thresholdBytes)
		return nil, written, err
	}
}

// streamToFile 以流式方式将对象写入文件
//
// 参数
//
//   - ctx context.Context: 上下文
//   - client (*minio.Client): minio客户端对象
//   - bucketName (string): 桶名称
//   - objectName (string): 文件名
//   - dstPath (string): 文件名——流式使用
//   - bufSize (int64): 缓冲大小
//
// 返回
//
//   - written: 写入
//   - error: 返回错误
//
//goland:noinspection GoUnhandledErrorResult
func streamToFile(ctx context.Context, client *minio.Client, bucketName, objectName, dstPath string, bufSize int64) (int64, error) {
	obj, err := client.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
	if err != nil {
		return 0, err
	}
	defer obj.Close()

	f, err := os.Create(dstPath)
	if err != nil {
		return 0, err
	}
	defer func() {
		// 不在此函数关闭 f 的使用者可能想在外部继续使用文件句柄，
		// 这里仍然 defer 关闭以避免泄漏；如果你需要返回打开的 *os.File，请修改实现。
		_ = f.Close()
	}()

	buf := make([]byte, bufSize) // e.g. 4MB
	written, err := io.CopyBuffer(f, obj, buf)
	if err != nil {
		return written, err
	}
	return written, nil
}

// StreamObjectToWriter 将对象流式写入任意 io.Writer（例如 http.ResponseWriter）
//
//goland:noinspection GoUnhandledErrorResult
func StreamObjectToWriter(ctx context.Context, w io.Writer, client *minio.Client,
	bucketName, objectName string, cacheSize int64) (int64, error) {
	if client == nil {
		return 0, fmt.Errorf("MinIO 客户端未初始化")
	}
	if objectName == "" {
		return 0, fmt.Errorf("文件名不能为空")
	}

	obj, err := client.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
	if err != nil {
		return 0, err
	}
	defer obj.Close()

	if cacheSize == 0 {
		cacheSize = 1024 * 1024
	}

	// 32KB = 32*1024 小缓冲，适用于网络传输；可根据场景调大
	buf := make([]byte, cacheSize)
	written, err := io.CopyBuffer(w, obj, buf)
	return written, err
}

// DownloadObjectWithResume 分块下载对象并支持断点续传
//
// 参数
//
//   - ctx context.Context: 上下文
//   - client (*minio.Client): minio客户端对象
//   - bucketName (string): 桶名称
//   - objectName (string): 文件名
//   - dstPath (string): 文件名——流式使用
//   - partSize (int64): 分片大小
//
// 返回
//
//   - error: 返回错误
//
//goland:noinspection GoUnhandledErrorResult
func DownloadObjectWithResume(ctx context.Context, client *minio.Client, bucketName, objectName, dstPath string, partSize int64) error {
	logger := zerolog.DefaultContextLogger
	// 先获取对象信息（大小）
	objInfo, err := client.StatObject(ctx, bucketName, objectName, minio.StatObjectOptions{})
	if err != nil {
		logger.Error().Err(err).Msgf("读取文件===%s出错", objectName)
		return fmt.Errorf("stat object error: %w", err)
	}
	objectSize := objInfo.Size

	// 检查本地文件是否已经存在，获取已下载大小
	var localSize int64
	if fi, statErr := os.Stat(dstPath); statErr == nil {
		logger.Error().Err(statErr).Msgf("文件===%s不存在", dstPath)
		localSize = fi.Size()
	} else if !os.IsNotExist(statErr) {
		logger.Error().Err(statErr).Msgf("文件===%s不存在", dstPath)
		return statErr
	}

	if localSize >= objectSize {
		logger.Debug().Msgf("文件已完整存在，本地大小=%d，对象大小=%d", localSize, objectSize)
		return nil
	}

	// 打开本地文件（追加写模式）
	f, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		logger.Error().Err(err).Msgf("打开文件===%s（追加写模式）错误", dstPath)
		return err
	}
	defer f.Close()

	// 从 localSize 继续下载
	start := localSize
	for start < objectSize {
		end := start + partSize - 1
		if end >= objectSize {
			end = objectSize - 1
		}

		logger.Debug().Msgf("下载分片===%s", fmt.Sprintf("bytes=%d-%d", start, end))

		// 构造 Range
		opts := minio.GetObjectOptions{}
		if rangeErr := opts.SetRange(start, end); rangeErr != nil {
			logger.Error().Err(rangeErr).Msg("构造分片range错误")
			return rangeErr
		}

		obj, getErr := client.GetObject(ctx, bucketName, objectName, opts)
		if getErr != nil {
			logger.Error().Err(getErr).Msg("获取文件错误")
			return getErr
		}

		// 移动到写入位置
		if _, seekErr := f.Seek(start, io.SeekStart); seekErr != nil {
			obj.Close()
			return seekErr
		}

		// 拷贝本段数据
		written, writeErr := io.Copy(f, obj)
		obj.Close()
		if writeErr != nil {
			return writeErr
		}

		start += written
		logger.Debug().Msgf("已下载 %d / %d 字节\n", start, objectSize)
	}

	absPath, _ := filepath.Abs(dstPath)

	logger.Debug().Msgf("下载完成，文件路径===%s", absPath)
	return nil
}

// StreamObjectListToZip 下载文件到zip
//
// 参数
//
//   - ctx context.Context: 上下文
//   - w io.Writer: 响应写入器
//   - client (*minio.Client): minio客户端对象
//   - bucketName (string): 桶名称
//   - objectList ([]string): 文件名切片
//   - cacheSize (int64): 缓冲区大小
//
// 返回
//
//   - error: 返回错误
//
//goland:noinspection GoUnhandledErrorResult
func StreamObjectListToZip(ctx context.Context, w io.Writer, client *minio.Client,
	bucketName string, objectList []string, cacheSize int64) error {
	logger := zerolog.DefaultContextLogger

	if len(objectList) == 0 {
		return errors.New("下载的文件不能为空")
	}

	logger.Debug().Msgf("桶名称===%s", bucketName)

	// 64KB缓存
	if cacheSize <= 0 {
		cacheSize = 64 * 1024
	}
	buf := make([]byte, cacheSize)

	// 创建管道
	pr, pw := io.Pipe()
	// 创建 ZIP 写入器
	zipWriter := zip.NewWriter(pw)

	go func() {
		defer func() {
			zipWriter.Close()
			pw.Close()
		}()

		for _, fileName := range objectList {
			decodedName, err := url.QueryUnescape(fileName)
			if err != nil {
				decodedName = fileName // 失败就用原始值
			}
			logger.Debug().Msgf("文件名称===%s", decodedName)
			// 获取对象信息
			statObject, err := client.StatObject(ctx, bucketName, decodedName, minio.StatObjectOptions{})
			if err != nil {
				logger.Error().Err(err).Msgf("minio检测文件===%s错误", decodedName)
				continue
			}

			object, err := client.GetObject(ctx, bucketName, decodedName, minio.GetObjectOptions{})
			if err != nil {
				logger.Error().Err(err).Msgf("minio下载文件==={}错误", decodedName)
				continue
			}

			func() {
				defer object.Close()

				// 生成 UUID 文件名
				uuidFileName := NormalUUID()
				if ext := filepath.Ext(decodedName); ext != "" {
					uuidFileName += ext
				}

				header := &zip.FileHeader{
					Name:     uuidFileName,
					Method:   zip.Deflate,
					Modified: statObject.LastModified,
				}
				header.SetMode(0644)
				header.UncompressedSize64 = uint64(statObject.Size)

				writer, err := zipWriter.CreateHeader(header)
				if err != nil {
					pw.CloseWithError(fmt.Errorf("创建 ZIP 文件写入器失败: %s===%+v", header.Name, err))
					return
				}

				/*if _, err = io.CopyBuffer(writer, object, buf); err != nil {
					pw.CloseWithError(fmt.Errorf("写入 ZIP 文件失败: %s===%+v", header.Name, err))
					return
				}*/

				// 包装成支持 ctx 取消的 reader
				reader := &ctxReader{ctx: ctx, r: object}

				if _, err = io.CopyBuffer(writer, reader, buf); err != nil {
					if errors.Is(err, context.Canceled) {
						pw.CloseWithError(fmt.Errorf("下载被取消: %s", decodedName))
						return
					}
					pw.CloseWithError(fmt.Errorf("写入 ZIP 文件失败: %s===%+v", header.Name, err))
					return
				}
			}()
		}
	}()

	/*if _, err := io.Copy(w, pr); err != nil {
		logger.Error().Err(err).Msg("写入响应失败")
		return errors.New("写入响应失败")
	}*/

	if _, err := io.Copy(w, pr); err != nil {
		// 如果是 ctx 取消，不记录为错误
		if !errors.Is(err, context.Canceled) {
			logger.Error().AnErr("写入响应失败", err).Msg("")
			return errors.New("写入响应失败")
		}
	}
	return nil
}

type ctxReader struct {
	ctx context.Context
	r   io.Reader
}

func (cr *ctxReader) Read(p []byte) (int, error) {
	select {
	case <-cr.ctx.Done():
		return 0, cr.ctx.Err() // 返回 context.Canceled
	default:
		return cr.r.Read(p)
	}
}

type zipItem struct {
	Name    string
	Body    io.ReadCloser
	Size    int64
	ModTime time.Time
	Err     error
}

// ParallelDownloadToZipConcurrent 并行下载文件到zip
//
// 参数
//
//   - ctx context.Context: 上下文
//   - w io.Writer: 响应写入器
//   - client (*minio.Client): minio客户端对象
//   - bucketName (string): 桶名称
//   - objectList ([]string): 文件名切片
//   - cacheSize (int64): 缓冲区大小
//   - concurrency (int64): 并行下载数量
//
// 返回
//
//   - error: 返回错误
//
//goland:noinspection GoUnhandledErrorResult
func ParallelDownloadToZipConcurrent(ctx context.Context, w io.Writer, client *minio.Client,
	bucketName string, objectList []string, cacheSize int64, concurrency int64) error {

	if len(objectList) == 0 {
		return errors.New("下载的文件不能为空")
	}
	if cacheSize <= 0 {
		cacheSize = 64 * 1024
	}
	// 默认同时下载 5 个文件
	if concurrency <= 0 {
		concurrency = 5
	}

	logger := zerolog.DefaultContextLogger
	buf := make([]byte, cacheSize)

	zipWriter := zip.NewWriter(w)
	defer zipWriter.Close()

	successList := make([]string, 0, len(objectList))
	failedList := make([]string, 0, len(objectList))

	itemCh := make(chan *zipItem, len(objectList))
	var wg sync.WaitGroup
	sem := make(chan struct{}, concurrency) // 控制并发数

	// 并发下载
	for _, objectName := range objectList {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}

		wg.Add(1)
		go func(objName string) {
			defer wg.Done()

			sem <- struct{}{}        // 获取信号量
			defer func() { <-sem }() // 释放信号量

			decodedName, err := url.QueryUnescape(objName)
			if err != nil {
				decodedName = objName // 失败就用原始值
			}

			statObject, err := client.StatObject(ctx, bucketName, decodedName, minio.StatObjectOptions{})
			if err != nil {
				logger.Error().Err(err).Msgf("minio检测文件 %s 错误", decodedName)
				itemCh <- &zipItem{Err: err}
				return
			}

			object, err := client.GetObject(ctx, bucketName, decodedName, minio.GetObjectOptions{})
			if err != nil {
				logger.Error().Err(err).Msgf("minio下载文件 %s 错误", decodedName)
				itemCh <- &zipItem{Err: err}
				return
			}

			uuidFileName := NormalUUID()
			if ext := filepath.Ext(decodedName); ext != "" {
				uuidFileName += ext
			}

			itemCh <- &zipItem{
				Name:    uuidFileName,
				Body:    object,
				Size:    statObject.Size,
				ModTime: statObject.LastModified,
			}

		}(objectName)
	}

	// 等待所有下载 goroutine 完成后关闭 channel
	go func() {
		wg.Wait()
		close(itemCh)
	}()

	// 单线程写 ZIP
	for item := range itemCh {
		if item.Err != nil {
			failedList = append(failedList, item.Name)
			continue
		}
		func() {
			defer item.Body.Close()

			header := &zip.FileHeader{
				Name:     item.Name,
				Method:   zip.Deflate,
				Modified: item.ModTime,
			}
			header.SetMode(0644)
			header.UncompressedSize64 = uint64(item.Size)

			writer, err := zipWriter.CreateHeader(header)
			if err != nil {
				logger.Error().Err(err).Msgf("创建 ZIP 文件写入器失败: %s", item.Name)
				failedList = append(failedList, item.Name)
				return
			}

			reader := &ctxReader{ctx: ctx, r: item.Body}

			if _, err := io.CopyBuffer(writer, reader, buf); err != nil {
				logger.Error().Err(err).Msgf("写入 ZIP 文件失败: %s", item.Name)
				if errors.Is(err, context.Canceled) {
					logger.Info().Msgf("下载被取消: %s", item.Name)
					return
				}
				logger.Error().Err(err).Msgf("写入 ZIP 文件失败: %s", item.Name)
			}

			successList = append(successList, item.Name)
		}()
	}

	// 写入 download_log.txt
	logHeader := &zip.FileHeader{
		Name:   "download_log.txt",
		Method: zip.Deflate,
	}
	logHeader.SetMode(0644)

	logWriter, err := zipWriter.CreateHeader(logHeader)
	if err != nil {
		logger.Error().Err(err).Msg("创建 download_log.txt 失败")
		return err
	}

	logBuf := bufio.NewWriter(logWriter)
	fmt.Fprintf(logBuf, "下载完成时间: %s\n", time.Now().Format("2006-01-02 15:04:05"))
	fmt.Fprintf(logBuf, "成功文件数: %d\n", len(successList))
	fmt.Fprintf(logBuf, "失败文件数: %d\n\n", len(failedList))

	fmt.Fprintf(logBuf, "原始文件:\n")
	for _, s := range objectList {
		decodedName, err := url.QueryUnescape(s)
		if err != nil {
			decodedName = s // 失败就用原始值
		}
		fmt.Fprintf(logBuf, "  - %s\n", decodedName)
	}
	fmt.Fprintln(logBuf)

	if len(successList) > 0 {
		fmt.Fprintf(logBuf, "成功文件:\n")
		for _, s := range successList {
			fmt.Fprintf(logBuf, "  - %s\n", s)
		}
		fmt.Fprintln(logBuf)
	}

	if len(failedList) > 0 {
		fmt.Fprintf(logBuf, "失败文件:\n")
		for _, f := range failedList {
			fmt.Fprintf(logBuf, "  - %s\n", f)
		}
	}

	logBuf.Flush()
	return nil
}
